package net.lxch.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import com.google.ortools.Loader;
import com.google.ortools.constraintsolver.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.lxch.config.AmapConfig;
import net.lxch.domain.*;
import net.lxch.domain.resp.ShipPlan;
import net.lxch.utils.SignUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class ShipPlanServiceImpl implements ShipPlanService {
    private final AmapConfig amapConfig;
    private final AmapService amapService;
    private final StringRedisTemplate redisTemplate;
    private final ObjectMapper objectMapper;

    @Override
    public ShipPlan getShortShipPlan(Point startPoint, List<ProductRecord> productRecords) {
        // 计算节点
        List<InterNode> interNodes = Lists.newArrayList();
        int cur = 0;
        // 加入起点
        interNodes.add(new InterNode(cur, startPoint, ""));
        for (ProductRecord record : productRecords)
            interNodes.add(new InterNode(++cur, record.getPoint(), record.getId()));

        // 计算各点之间的距离
        long[][] distanceMatrix = computeDistance(interNodes);

        // 获取最短路由和最短路径
        SolutionRet solution = getSolution(distanceMatrix);

        // 根据最短路由和最短路径返回配送计划
        return getPlan(interNodes, productRecords, solution);
    }

    private static ShipPlan getPlan(List<InterNode> interNodes, List<ProductRecord> productRecords, SolutionRet solution) {
        Map<Integer, InterNode> interNodeMap = interNodes.stream()
                .collect(Collectors.toMap(InterNode::getId, Function.identity()));

        Map<String, ProductRecord> productRecordMap = productRecords.stream()
                .collect(Collectors.toMap(ProductRecord::getId, Function.identity()));

        ShipPlan shipPlan = new ShipPlan();
        shipPlan.setDistance(solution.getDistance());
        solution.getRouter().forEach(item -> {
            String recordId = interNodeMap.get(item).getRecordId();
            if (Objects.nonNull(productRecordMap.get(recordId)))
                shipPlan.getPlan().add(productRecordMap.get(recordId));
        });
        solution.getRouter().forEach(item -> shipPlan.getRoute().add(interNodeMap.get(item)));
        return shipPlan;
    }

    // 选取最优配送并返回
    static SolutionRet getSolution(long[][] distanceMatrix) {
        Loader.loadNativeLibraries();
        RoutingIndexManager manager =
                new RoutingIndexManager(distanceMatrix.length, 1, 0);

        RoutingModel routing = new RoutingModel(manager);

        final int transitCallbackIndex =
                routing.registerTransitCallback((long fromIndex, long toIndex) -> {
                    int fromNode = manager.indexToNode(fromIndex);
                    int toNode = manager.indexToNode(toIndex);
                    return distanceMatrix[fromNode][toNode];
                });
        routing.setArcCostEvaluatorOfAllVehicles(transitCallbackIndex);
        RoutingSearchParameters searchParameters =
                main.defaultRoutingSearchParameters()
                        .toBuilder()
                        .setFirstSolutionStrategy(FirstSolutionStrategy.Value.PATH_CHEAPEST_ARC)
                        .build();

        Assignment solution = routing.solveWithParameters(searchParameters);
        return printSolution(routing, manager, solution);
    }

    private static SolutionRet printSolution(RoutingModel routing, RoutingIndexManager manager, Assignment solution) {

        SolutionRet ret = new SolutionRet();

        long index = routing.start(0);
        while (!routing.isEnd(index)) {
            ret.getRouter().add(manager.indexToNode(index));
            long previousIndex = index;
            index = solution.value(routing.nextVar(index));
            ret.setDistance(ret.getDistance() + routing.getArcCostForVehicle(previousIndex, index, 0));
        }
        ret.getRouter().add(manager.indexToNode(routing.end(0)));

        return ret;
    }

    /**
     * 计算每两个点之间的距离
     */
    private long[][] computeDistance(List<InterNode> interNodes) {
        long[][] distanceMatrix = new long[interNodes.size()][interNodes.size()];

        interNodes.parallelStream()
                .forEachOrdered(item -> interNodes
                        .forEach(item2 ->
                                distanceMatrix[item.getId()][item2.getId()]
                                        = getDistanceByAmap(item.getPoint(), item2.getPoint())));

//        for (int i = 0; i < interNodes.size(); i++) {
//            for (int j = 0; j < interNodes.size(); j++) {
//                distanceMatrix[i][j] = GPSDistanceUtil.getDistance(interNodes.get(i).getPoint(), interNodes.get(j).getPoint());
//            }
//        }
        return distanceMatrix;
    }

    private long getDistanceByAmap(Point point1, Point point2) {
        AmapDistanceRet ret = getDirection(point1, point2);
        if (Objects.nonNull(ret)
                && Objects.nonNull(ret.getRoute())
                && Objects.nonNull(ret.getRoute().getPaths())
                && ret.getRoute().getPaths().size() > 0) {
            // 如果起点终点相同则返回0
            if (Objects.equals(point1.getLng(), point2.getLng()) && Objects.equals(point1.getLat(), point2.getLat()))
                return 0L;

            return Long.parseLong(ret.getRoute().getPaths().get(0).getDistance());
        }
        return 0L;
    }

    public AmapDistanceRet getDirection(Point point1, Point point2) {
        AmapDistanceRet ret = null;

        String redisKey = String.format("%s-%s:%s:%s:%s", "AMAP_DIRECTION", point1.getLng(), point1.getLat(), point2.getLng(), point2.getLat());
        // 是否已存在
        String str = redisTemplate.opsForValue().get(redisKey);
        if (StringUtils.isNotBlank(str)) {
            try {
                ret = objectMapper.readValue(str, AmapDistanceRet.class);
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        }

        if (Objects.isNull(ret)) {
            // 签名
            Map<String, Object> params = new HashMap<>();
            params.put("origin", point1.getLng() + "," + point1.getLat());
            params.put("destination", point2.getLng() + "," + point2.getLat());
            params.put("output", "json");
            params.put("key", amapConfig.getAmapKey());
            String sign = SignUtil.getSign(params, amapConfig.getAmapSecret());

            ret = amapService.getDrivingDirection(point1.getLng() + "," + point1.getLat(),
                    point2.getLng() + "," + point2.getLat(),
                    "json",
                    amapConfig.getAmapKey(),
                    sign);

            if (Objects.nonNull(ret)) {
                try {
                    redisTemplate.opsForValue().set(redisKey, objectMapper.writeValueAsString(ret), 10, TimeUnit.DAYS);
                } catch (JsonProcessingException e) {
                    throw new RuntimeException(e);
                }
            }
        }

        return ret;
    }
}
